package net.tools.web;

import java.io.*;
import java.net.*;

public class TunnelClient implements Runnable
{

	private static String tunnelurl;

	private static void toBytes(long l, byte[] b, int offset, int len)
	{
		for(int i = 0; i < len; i++)
			b[i + offset] = (byte) (l >> (len * 8 - 8 - i * 8));
	}

	private static long fromBytes(byte[] b, int offset, int len)
	{
		long l = 0;
		for(int i = 0; i < len; i++)
			l |= ((long) (b[i + offset] & 0x00ff)) << (len * 8 - 8 - i * 8);
		return l;
	}

	private static void writeTo(InputStream in, OutputStream out, byte[] buf) throws IOException
	{
		if(buf == null)
			buf = new byte[65536];
		int len;
		while((len = in.read(buf)) > 0)
		{
			out.write(buf, 0, len);
			out.flush();
		}
	}

	private static int readByte(InputStream in) throws IOException
	{
		int b = in.read();
		if(b < 0)
			throw new EOFException();
		return b;
	}

	private static void fill(InputStream in, byte[] buf, int offset, int len) throws IOException
	{
		while(len > 0)
		{
			int n = in.read(buf, offset, len);
			if(n <= 0)
				throw new EOFException();
			offset += n;
			len -= n;
		}
	}

	private static class Tunnelling implements Runnable
	{

		private Socket ssock;

		private InputStream in;

		private OutputStream out;

		private byte[] sessionId;

		private String tunnelurl;

		private Tunnelling(Socket ssock, InputStream in, OutputStream out, byte[] sessionId, String tunnelurl)
		{
			this.ssock = ssock;
			this.in = in;
			this.out = out;
			this.sessionId = sessionId;
			this.tunnelurl = tunnelurl;
		}

		public void run()
		{
			byte[] buf = new byte[65536];
			try
			{
				while(true)
				{
					URLConnection conn = new URL(tunnelurl).openConnection();
					conn.setDoOutput(true);
					OutputStream out = conn.getOutputStream();
					out.write(2);
					out.write(sessionId);
					out.flush();
					InputStream in = conn.getInputStream();
					if(in.read() != 1)
						throw new EOFException();
					writeTo(in, ssock.getOutputStream(), buf);
				}
			}
			catch (IOException e)
			{
				//e.printStackTrace();
			}
			finally
			{
				try
				{
					ssock.close();
				}
				catch (Exception e)
				{}
				try
				{
					in.close();
				}
				catch (Exception e)
				{}
				try
				{
					out.close();
				}
				catch (Exception e)
				{}
				try
				{
					URLConnection conn = new URL(tunnelurl).openConnection();
					OutputStream out = conn.getOutputStream();
					out.write(4);
					out.write(sessionId);
					out.flush();
					conn.getInputStream();
				}
				catch (Exception e)
				{}
			}
		}
		
	}

	private Socket ssock;

	private TunnelClient(Socket ssock)
	{
		this.ssock = ssock;
	}

	public void run()
	{
		InputStream in = null;
		try
		{
			OutputStream sout = ssock.getOutputStream();
			InputStream sin = ssock.getInputStream();
			readByte(sin);
			int len = readByte(sin);
			for(int i = 0; i < len; i++)
				readByte(sin);
			sout.write(5);
			sout.write(0);
			readByte(sin);
			int cmd = readByte(sin);
			readByte(sin);
			int addrtype = readByte(sin);
			String remoteaddress = null;
			byte[] buf = new byte[65536];
			switch(addrtype)
			{
			case 1:
				{
					remoteaddress = "" + readByte(sin);
					for(int i = 1; i < 4; i++)
					{
						remoteaddress += ".";
						remoteaddress += readByte(sin);
					}
				}
				break;
			case 3:
				{
					len = readByte(sin);
					fill(sin, buf, 0, len);
					remoteaddress = new String(buf, 0, len);
				}
				break;
			case 4:
				throw new IOException("No accept address type 4");
			default:
				throw new IOException("No accept address type " + addrtype);
			}
			fill(sin, buf, 0, 2);
			switch(cmd)
			{
			case 1:
				URLConnection conn = new URL(tunnelurl).openConnection();
				conn.setDoOutput(true);
				OutputStream out = conn.getOutputStream();
				out.write(1);
				byte[] addr = remoteaddress.getBytes();
				out.write(addr.length);
				out.write(addr);
				out.write(buf, 0, 2);//port
				out.flush();
				in = conn.getInputStream();
				if(readByte(in) != 1)
					throw new EOFException();
				byte[] sessionId = new byte[8];
				fill(in, sessionId, 0, 8);
				sout.write(5);
				sout.write(0);
				sout.write(0);
				sout.write(1);
				sout.write(0);
				sout.write(0);
				sout.write(0);
				sout.write(0);
				sout.write(0);
				sout.write(0);
				sout.flush();
				new Thread(new Tunnelling(ssock, in, sout, sessionId, tunnelurl)).start();
				while((len = sin.read(buf)) > 0)
				{
					conn = new URL(tunnelurl).openConnection();
					conn.setDoOutput(true);
					out = conn.getOutputStream();
					out.write(3);
					out.write(sessionId);
					out.write(buf, 0, len);
					out.flush();
					if(conn.getInputStream().read() != 1)
						throw new EOFException();
				}
				break;
			case 2:
				throw new IOException("No accept command.");
			case 3:
				throw new IOException("No accept command.");
			default:
				throw new IOException("No accept command.");
			}
		}
		catch (IOException e)
		{
			//e.printStackTrace();
		}
		finally
		{
			try
			{
				ssock.close();
			}
			catch (Exception e)
			{}
			try
			{
				in.close();
			}
			catch (Exception e)
			{}
		}
	}

	public static void main(String[] args) throws Exception
	{
		ServerSocket sssock = new ServerSocket(Integer.parseInt(args[0]));
		tunnelurl = args[1];
		while(true)
		{
			Socket ssock = sssock.accept();
			new Thread(new TunnelClient(ssock)).start();
		}
	}

}
